package com.fept.msgcore.msgserver.protocol.frame;

import com.fept.msgcore.msgserver.common.model.ToString;
import com.fept.msgcore.msgserver.protocol.exception.ProtocolErrorCodeEnum;
import com.fept.msgcore.msgserver.protocol.exception.ProtocolException;
import com.fept.msgcore.msgserver.protocol.model.base.BaseTLVSegment;
import com.fept.msgcore.msgserver.protocol.model.base.ProtocolConstants;
import com.fept.msgcore.msgserver.protocol.model.body.*;
import com.fept.msgcore.msgserver.protocol.model.header.Header;
import com.fept.msgcore.msgserver.protocol.model.option.*;
import com.fept.msgcore.msgserver.protocol.util.FrameHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by Srma on 1/13/2017.
 */
public class Frame extends ToString {
    private static final Logger logger = LoggerFactory.getLogger(Frame.class);

    /**
     *
     */
    private static final long serialVersionUID = -743731207825162928L;

    private String ip;
    private int port;

    private long receivedTime;
    private long processedTime;

    private ProtocolErrorCodeEnum errorCode = ProtocolErrorCodeEnum.SUCCESS;
    private Header header;
    private List<BaseOption> options = new ArrayList<BaseOption>();
    private List<BaseBody> bodies = new ArrayList<BaseBody>();

    /**
     *
     * @param frame
     * @return
     */
    public byte[] pack() {
        try {
            /**
             * pack body
             */
            int bodyTotalSize = 0;
            byte[][] bodiesData = new byte[this.getBodies().size()][];
            for (int i = 0; i < this.getBodies().size(); i++) {
                BaseTLVSegment bodySeg = this.getBodies().get(i);
                bodiesData[i] = bodySeg.pack();
                bodyTotalSize += bodiesData[i].length;
            }

            int bodyOffset = 0;
            byte[] bodyBuf = new byte[bodyTotalSize];
            for (byte[] bodyBytes : bodiesData) {
                System.arraycopy(bodyBytes, 0, bodyBuf, bodyOffset,
                        bodyBytes.length);
                bodyOffset += bodyBytes.length;
            }

            /**
             * pack options
             */
            int optionTotalSize = 0;
            byte[][] optionsData = new byte[this.getOptions().size()][];
            for (int i = 0; i < this.getOptions().size(); i++) {
                BaseTLVSegment option = this.getOptions().get(i);
                switch (option.getType()) {
                    case ProtocolConstants.OPTION_NOP:
                        optionsData[i] = ((NopOption) option).pack();
                        optionTotalSize += optionsData[i].length;
                        break;
                    case ProtocolConstants.OPTION_ANSWER:
                        optionsData[i] = ((AnswerOption) option).pack();
                        optionTotalSize += optionsData[i].length;
                        break;
                    case ProtocolConstants.OPTION_SIGN:
                        ((SignOption) option).setMac(FrameHelper.sign(
                                ((SignOption) option), bodyBuf));
                        optionsData[i] = option.pack();
                        optionTotalSize += optionsData[i].length;
                        break;
                    case ProtocolConstants.OPTION_ENCRYPT:
                        bodyBuf = FrameHelper.encryptData((EncryptOption) option,
                                bodyBuf);
                        optionsData[i] = option.pack();
                        optionTotalSize += optionsData[i].length;
                        break;
                    default:
                        logger.error(String.format(
                                "option %04X not supported, option ignored!",
                                option.getType()));
                        break;
                }
            }

            int optionOffset = 0;
            byte[] optionBuf = new byte[optionTotalSize];
            for (byte[] optionBytes : optionsData) {
                System.arraycopy(optionBytes, 0, optionBuf, optionOffset,
                        optionBytes.length);
                optionOffset += optionBytes.length;
            }

            /**
             * pack header
             */
            this.getHeader().setBodyNum(bodiesData.length);
            this.getHeader().setOptionNum(optionsData.length);
            this.getHeader().setTimestamp(System.currentTimeMillis());
            this.getHeader().setTotalLength(
                    this.getHeader().getBytesLength() + optionTotalSize
                            +bodyBuf.length );
            //							+ bodyTotalSize);
            byte[] headerBuf = this.getHeader().pack();


            /**
             * pack frame
             */
            int frameOffset = 0;
            byte[] frameBuf = new byte[this.getHeader().getTotalLength()];
            /**
             * patch header
             */
            System.arraycopy(headerBuf, 0, frameBuf, frameOffset,
                    headerBuf.length);
            frameOffset += headerBuf.length;


            /**
             * patch options
             */
            System.arraycopy(optionBuf, 0, frameBuf, frameOffset,
                    optionBuf.length);
            frameOffset += optionBuf.length;

            /**
             * patch bodies
             */
            System.arraycopy(bodyBuf, 0, frameBuf, frameOffset, bodyBuf.length);
            return frameBuf;
        } catch (Exception e) {
            logger.error("exception on frame packing");
//            logger.exception(e);
        }
        return null;
    }

    /**
     *
     * @param headerData
     * @param data
     */
    public void unpack(byte[] headerData, byte[] data) {
        this.header = new Header();
        this.header.unpack(headerData, 0);

        try {
            int offset = 0;

            /**
             * 加载选项TLV
             */
            for (int i = 0; i < header.getOptionNum(); i++) {
                BaseTLVSegment option = new BaseTLVSegment();
                option.unpack(data, offset);
                switch (option.getType()) {
                    case ProtocolConstants.OPTION_NOP:
                        NopOption nopOption = new NopOption();
                        nopOption.unpack(data, offset);
                        this.getOptions().add(nopOption);
                        break;
                    case ProtocolConstants.OPTION_ANSWER:
                        AnswerOption answerOption = new AnswerOption();
                        answerOption.unpack(data, offset);
                        this.getOptions().add(answerOption);
                        break;
                    case ProtocolConstants.OPTION_SIGN:
                        SignOption signOption = new SignOption();
                        signOption.unpack(data, offset);
                        this.getOptions().add(signOption);
                        break;
                    case ProtocolConstants.OPTION_ENCRYPT:
                        EncryptOption encryptOption = new EncryptOption();
                        encryptOption.unpack(data, offset);
                        this.getOptions().add(encryptOption);
                        break;
                    default:
                        logger.error(String.format(
                                "option %04X not supported, option ignored!",
                                option.getType()));
                        break;
                }
                offset += option.getBytesLength();
            }

            /**
             * 截取帧体数据
             */
            byte[] bodyData = new byte[data.length - offset];
            System.arraycopy(data, offset, bodyData, 0, bodyData.length);

            /**
             * 解析选项
             */
            for (int i = this.getOptions().size() - 1; i >= 0; i--) {
                BaseOption baseOption = this.getOptions().get(i);
                switch (baseOption.getType()) {
                    case ProtocolConstants.OPTION_NOP:
                    case ProtocolConstants.OPTION_ANSWER:
                        break;
                    case ProtocolConstants.OPTION_SIGN:
                        try {
                            boolean result = FrameHelper.validateSign(
                                    (SignOption) baseOption, bodyData);
                            if (!result) {
                                throw new ProtocolException(
                                        ProtocolErrorCodeEnum.SIGN_VALIDATION_ERROR);
                            }
                        } catch (Exception e) {
                            logger.error("exception on validating signature of frame data");
                            throw new ProtocolException(
                                    ProtocolErrorCodeEnum.SIGN_VALIDATION_ERROR);
                        }
                        break;
                    case ProtocolConstants.OPTION_ENCRYPT:
                        try {
                            bodyData = FrameHelper.decryptData(
                                    (EncryptOption) baseOption, bodyData);
                        } catch (Exception e) {
                            logger.error("exception on decryption frame data");
                            logger.error(e.getMessage());
                            throw new ProtocolException(
                                    ProtocolErrorCodeEnum.DECRYPTION_ERROR);
                        }
                        break;

                    default:
                        logger.error(String.format(
                                "option %04X not supported, option ignored!",
                                baseOption.getType()));
                        break;
                }
            }

            int bodyOffset = 0;
            /**
             * 加载选项TLV
             */
            for (int i = 0; i < header.getBodyNum(); i++) {
                BaseTLVSegment body = new BaseTLVSegment();
                body.unpack(bodyData, bodyOffset);
                switch (body.getType()) {
                    case ProtocolConstants.BODY_NOP:
                        NopBody nopBody = new NopBody();
                        nopBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(nopBody);
                        break;
                    case ProtocolConstants.BODY_COMMAND:
                        CommandBody commandBody = new CommandBody();
                        commandBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(commandBody);
                        break;
                    case ProtocolConstants.BODY_EVENT:
                        EventBody eventBody = new EventBody();
                        eventBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(eventBody);
                        break;
                    case ProtocolConstants.BODY_SESSION:
                        SessionBody sessionBody = new SessionBody();
                        sessionBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(sessionBody);
                        break;
                    case ProtocolConstants.BODY_SETTING:
                        SettingBody settingBody = new SettingBody();
                        settingBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(settingBody);
                        break;
                    case ProtocolConstants.BODY_DATA:
                        DataBody dataBody = new DataBody();
                        dataBody.unpack(bodyData, bodyOffset);
                        this.getBodies().add(dataBody);
                        break;
                    default:
                        logger.error(String.format(
                                "body %04X not supported, invalid frame!",
                                body.getType()));
                        throw new ProtocolException(
                                ProtocolErrorCodeEnum.INVALID_FRAME_BODY);
                }
                bodyOffset += body.getBytesLength();
            }
        } catch (Exception e) {
            if (e instanceof ProtocolException) {
                this.setErrorCode(((ProtocolException) e).getErrorCode());
            } else {
                this.setErrorCode(ProtocolErrorCodeEnum.INVALID_DATA);
            }
            logger.error("exception on frame unpacking");
            logger.error(e.getMessage());
        }

        /**
         * 完成帧接收工作，设置接收时间
         */
        this.receivedTime = System.currentTimeMillis();
    }

    /**
     * @return the receivedTime
     */
    public Long getReceivedTime() {
        return receivedTime;
    }

    /**
     * @param receivedTime
     *            the receivedTime to set
     */
    public void setReceivedTime(Long receivedTime) {
        this.receivedTime = receivedTime;
    }

    /**
     * @return the processedTime
     */
    public Long getProcessedTime() {
        return processedTime;
    }

    /**
     * @param processedTime
     *            the processedTime to set
     */
    public void setProcessedTime(Long processedTime) {
        this.processedTime = processedTime;
    }

    /**
     * @return the header
     */
    public Header getHeader() {
        return header;
    }

    /**
     * @param header
     *            the header to set
     */
    public void setHeader(Header header) {
        this.header = header;
    }

    /**
     * @return the options
     */
    public List<BaseOption> getOptions() {
        return options;
    }

    /**
     * @param options
     *            the options to set
     */
    public void setOptions(List<BaseOption> options) {
        this.options = options;
    }

    /**
     * @return the bodys
     */
    public List<BaseBody> getBodies() {
        return bodies;
    }

    /**
     * @param bodys
     *            the bodys to set
     */
    public void setBodies(List<BaseBody> bodies) {
        this.bodies = bodies;
    }

    /**
     * @param receivedTime
     *            the receivedTime to set
     */
    public void setReceivedTime(long receivedTime) {
        this.receivedTime = receivedTime;
    }

    /**
     * @param processedTime
     *            the processedTime to set
     */
    public void setProcessedTime(long processedTime) {
        this.processedTime = processedTime;
    }

    /**
     * @return the errorCode
     */
    public ProtocolErrorCodeEnum getErrorCode() {
        return errorCode;
    }

    /**
     * @param errorCode
     *            the errorCode to set
     */
    public void setErrorCode(ProtocolErrorCodeEnum errorCode) {
        this.errorCode = errorCode;
    }

    /**
     * @return the ip
     */
    public String getIp() {
        return ip;
    }

    /**
     * @param ip
     *            the ip to set
     */
    public void setIp(String ip) {
        this.ip = ip;
    }

    /**
     * @return the port
     */
    public int getPort() {
        return port;
    }

    /**
     * @param port
     *            the port to set
     */
    public void setPort(int port) {
        this.port = port;
    }
}
